package t.l;

import java.awt.Color;
import java.awt.HeadlessException;
import java.awt.color.ColorSpace;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.Timer;
import t.l.component.Person;
import t.l.component.Table;

public class MFrame extends JFrame {

  private List<Person> personList;
  private List<Person> removedList = new ArrayList<>();
  private double tableDiameter;
  private Table table;

  private Controller controller = new Controller();

  public Controller getController() {
    return controller;
  }

  public MFrame(double width, double height)
      throws HeadlessException {
    this(width, height, width / 3, height / 2, height * 2 / 3);
  }

  public MFrame(double width, double height, double tableCenterX,
      double tableCenterY, double tableDiameter) throws HeadlessException {
    super();
    this.tableDiameter = tableDiameter;
    setLayout(null);
    this.setSize(((int) width), ((int) height));
    initFrame(tableCenterX, tableCenterY, tableDiameter);
  }

  public double getTableDiameter() {
    return tableDiameter;
  }

  private void initFrame(double centerX, double centerY,
      double tableDiameter) {
    table = new Table();
    table.setBounds((int) (centerX - tableDiameter / 2), (int) (centerY - tableDiameter / 2),
        (int) tableDiameter, (int) tableDiameter);
  }

  public void resetPersonList(List<Person> personList) {
    if (this.personList != null && this.personList.size() > 0) {
      this.personList.forEach(this::remove);
    }
    this.personList = personList;
    removedList.clear();
    if (personList != null && !personList.isEmpty()) {
      updatePersonsPosition();
      personList.forEach(this::add);
      remove(table);
      add(table);
    }
    repaint();
  }

  public void updateRemovedPerson(List<Person> removedList) {
    this.removedList.clear();
    if (Util.notEmpty(removedList)) {
      this.removedList.addAll(removedList);
    }
  }

  private void updatePersonsPosition() {
    int n = personList.size();
    double tableRadius = tableDiameter / 2;
    double tableCenterX = table.getX() + tableRadius;
    double tableCenterY = table.getY() + tableRadius;
    for (int i = 0; i < personList.size(); i++) {
      Person person = personList.get(i);
      double angle = i * 1.0 / n * 2 * Math.PI;
      person.refreshBound(
          (int) (tableCenterX + tableRadius * Math.sin(angle) - person.getDiameter() / 2),
          (int) (tableCenterY - tableRadius * Math.cos(angle) - person.getDiameter() / 2));
    }
  }

  private void personLoopAnimate(int index) {
    int previousIndex = index > 0 ? index - 1 : personList.size() - 1;
    personList.get(previousIndex).setColor(Person.NORMAL_COLOR);
    personList.get(index).setColor(Person.CHOSE_COLOR);
    personList.get(previousIndex).repaint();
    personList.get(index).repaint();
  }

  private RemoveAnimator animRunner = new RemoveAnimator() {
    @Override public void run() {
      while (!isAnimationEnd()) {
        update();
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          e.printStackTrace();
          break;
        }
      }
      personList.remove(person);
      remove(person);
      removedList.add(person);
      updatePersonsPosition();
      repaint();
      controller.resetCurrentCount();
      if (controller.isLooping()) {
        controller.restartAnimate();
      }
    }
  };

  private void removePerson(int index) {
    int tableCenterX = table.getX() + table.getWidth() / 2;
    int tableCenterY = table.getY() + table.getHeight() / 2;
    double animateDistance = 200.0;
    Person person = personList.get(index);
    int radius = person.getDiameter() / 2;
    int longSide = table.getWidth() / 2 + radius;
    int times = 100;
    int spaceX = (int) ((person.getX() - tableCenterX) * 1.0 / longSide * animateDistance / times);
    int spaceY = (int) ((person.getY() - tableCenterY) * 1.0 / longSide * animateDistance / times);
    ScheduledExecutorService rmPersonExecutorService = new ScheduledThreadPoolExecutor(1);

    animRunner.reset(person, spaceX,
        spaceY, times);

    rmPersonExecutorService.schedule(animRunner, 500, TimeUnit.MILLISECONDS);
  }

  private static abstract class RemoveAnimator implements Runnable {
    Person person;
    private int spaceX = 20;
    private int spaceY = 20;
    private float animTimes = 0.0f;
    private int maxAnimTimes;
    private Color personColor;
    private float[] colorComponents;
    private ColorSpace colorSpace;

    void reset(Person person, int spaceX, int spaceY, int maxAnimTimes) {
      this.person = person;
      this.spaceX = spaceX;
      this.spaceY = spaceY;
      this.maxAnimTimes = maxAnimTimes;
      this.personColor = person.getColor();
      colorComponents = personColor.getColorComponents(null);
      colorSpace = personColor.getColorSpace();
      animTimes = 0.0f;
    }

    void update() {
      person.setColor(
          new Color(colorSpace, colorComponents, ((maxAnimTimes - animTimes) / maxAnimTimes)));
      person.setBounds(person.getX() + spaceX, person.getY() + spaceY, person.getWidth(),
          person.getHeight());
      person.repaint();
      animTimes++;
    }

    boolean isAnimationEnd() {
      return animTimes >= maxAnimTimes;
    }
  }

  public class Controller implements ActionListener {
    private LoopStatusController loopStatusController = new LoopStatusController();
    private FrameView frameView;
    private int m, runtimeK;
    private int currentCount = 0;
    private int animInterval = 1000;

    private Timer timer;

    public void setM(int m) {
      this.m = m;
    }

    public void setRuntimeK(int runtimeK) {
      this.runtimeK = runtimeK;
    }

    public void setAnimInterval(int animInterval) {
      this.animInterval = animInterval;
    }

    public void setFrameView(FrameView frameView) {
      this.frameView = frameView;
    }

    public boolean isLooping() {
      return loopStatusController.isLoopStarted && !loopStatusController.isPaused;
    }

    public boolean isLoopStarted() {
      return loopStatusController.isLoopStarted;
    }

    public void setCurrentCount(int currentCount) {
      this.currentCount = currentCount;
    }

    public void resetCurrentCount() {
      currentCount = 0;
    }

    public void startLoop() {
      if (!loopStatusController.isLoopStarted) {
        timer = new Timer(animInterval, this);
        timer.setInitialDelay(0);
        restartAnimate(currentCount);
      }
      loopStatusController.setLoopStarted(true);
    }

    public void cancelLoop() {
      pauseAnim();
      loopStatusController.setLoopStarted(false);
      currentCount = 0;
    }

    public void pauseAnim() {
      if (isLooping() && timer != null && timer.isRunning()) {
        timer.stop();
      }
      loopStatusController.setPaused(true);
    }

    public void resumeLoop() {
      loopStatusController.setPaused(false);
      timer.restart();
    }

    private void restartAnimate(int currentCount) {
      if (personList.size() == 0) {
        return;
      }

      if (runtimeK >= personList.size()) {
        runtimeK = runtimeK % personList.size();
      } else if (runtimeK < 0) {
        Util.toast("k must larger than zero", "Illeagl input", JOptionPane.ERROR_MESSAGE);
        return;
      }

      if (m < 1) {
        Util.toast("m must larger than zero", "Illegal input", JOptionPane.ERROR_MESSAGE);
        return;
      }

      if (personList.size() == 1) {
        removePerson(0);
        loopStatusController.setLoopStarted(false);
        frameView.endLoop();
        return;
      }
      this.currentCount = currentCount;
      resumeLoop();
    }

    private void restartAnimate() {
      restartAnimate(0);
    }

    public void saveToDisk(int n, int k, File file) {
      RuntimeData runtimeData =
          new RuntimeData(m, k, runtimeK, n, animInterval, currentCount, personList, removedList);
      String data = Util.gson.toJson(runtimeData);
      new Thread(() -> {
        file.delete();
        try {
          file.createNewFile();
          if (file.canWrite()) {
            FileWriter fileWriter = new FileWriter(file);
            fileWriter.write(data);
            fileWriter.flush();
            fileWriter.close();
          }
        } catch (IOException e) {
          e.printStackTrace();
          Util.toast("save data to disk failed, see consola for more message", "Save data error",
              JOptionPane.ERROR_MESSAGE);
        }
      }).start();
    }

    @Override public void actionPerformed(ActionEvent e) {
      if (Util.notEmpty(personList)) {
        currentCount++;
        runtimeK = runtimeK < personList.size() ? runtimeK : runtimeK % personList.size();
        personLoopAnimate(runtimeK);
        frameView.loopAnimating(currentCount);
        runtimeK = ++runtimeK;
        if (currentCount >= m) {
          timer.stop();
          runtimeK = --runtimeK;
          removePerson(runtimeK);
        }
      }
    }

    private class LoopStatusController {
      private boolean isLoopStarted;
      private boolean isPaused = false;

      void setLoopStarted(boolean loopStarted) {
        isLoopStarted = loopStarted;
        frameView.isLooping(loopStarted);
      }

      public void setPaused(boolean paused) {
        isPaused = paused;
        frameView.isLooping(!paused);
      }
    }
  }
}
